/*
 * Copyright 2018 xiaomaoguai.com All right reserved. This software is the
 * confidential and proprietary information of xiaomaoguai.com ("Confidential
 * Information"). You shall not disclose such Confidential Information and shall
 * use it only in accordance with the terms of the license agreement you entered
 * into with xiaomaoguai.com.
 */

package com.xiaomaoguai.fcp.pre.kepler.redis.aliyun;


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.xiaomaoguai.fcp.pre.kepler.redis.configuration.RedisProperties;
import com.xiaomaoguai.fcp.pre.kepler.redis.constants.CacheConstants;
import com.xiaomaoguai.fcp.pre.kepler.redis.constants.DeployEnvEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.zookeeper.ZooKeeper;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;

import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;

/**
 * An {@link ApplicationContextInitializer} ，获取"阿里云 Redis"连接信息进行自动配置。
 * <p>
 * 会配置以下几个节点key，可以在项目中的 application.yml 中进行覆盖
 *
 * <pre>
 * map.put(&quot;spring.redis.host&quot;, redisInfo.getHost());
 * map.put(&quot;spring.redis.port&quot;, redisInfo.getPort());
 * map.put(&quot;spring.redis.database&quot;, redisInfo.getDbNo());
 * map.put(&quot;spring.redis.timeout&quot;, &quot;1s&quot;);
 * map.put(&quot;spring.redis.password&quot;, redisInfo.getPassword());
 * </pre>
 *
 * @author chenyao
 * @see org.springframework.boot.SpringApplication#applyInitializers(org.springframework.context.ConfigurableApplicationContext)
 * @since 2018年5月22日 下午6:49:09
 */
@Slf4j
@EnableConfigurationProperties(RedisProperties.class)
public class AliYunRedisEnvironmentPropertySourceLocator
		implements ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

	/**
	 * The default order for the processor.
	 *
	 * <pre>
	 * 如果项目中有这个依赖
	 * &lt;dependency&gt;
	 *     &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
	 *     &lt;artifactId&gt;spring-cloud-context&lt;/artifactId&gt;
	 * &lt;/dependency&gt;
	 * 会在它后面执行初始化
	 * {@code org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#getOrder()}
	 *
	 * 这样就可以支持从动态配置中心中获取配置的 redisKey
	 * &lt;dependency&gt;
	 *     &lt;groupId&gt;org.springframework.cloud&lt;/groupId&gt;
	 *     &lt;artifactId&gt;spring-cloud-starter-config&lt;/artifactId&gt;
	 * &lt;/dependency&gt;
	 * </pre>
	 */
	public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 11;

	/**
	 * 属性配置名字
	 */
	public static final String PROPERTY_SOURCE_NAME = "defaultProperties";

	/**
	 * Bootstrap Property Source Name:
	 * {@code org.springframework.cloud.bootstrap.config.PropertySourceBootstrapConfiguration#BOOTSTRAP_PROPERTY_SOURCE_NAME}
	 */
	public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = "bootstrapProperties";

	/**
	 * zookeeper 测试环境地址
	 */
	public static final String ZK_SERVER_TEST = "10.139.103.1:2181";

	/**
	 * zookeeper 预发环境地址
	 */
	public static final String ZK_SERVER_PRE = "zk.za.net:2181";

	/**
	 * zookeeper 生产环境地址
	 */
	public static final String ZK_SERVER_PRD = "zk.za.net:2181";

	@Override
	public int getOrder() {
		return DEFAULT_ORDER;
	}

	@Override
	public void initialize(ConfigurableApplicationContext applicationContext) {
		ConfigurableEnvironment environment = applicationContext.getEnvironment();
		Binder binder = Binder.get(environment);
		BindResult<RedisProperties> bindResult = binder.bind(CacheConstants.REDIS_PREFIX, RedisProperties.class);
		if (!bindResult.isBound()) {
			return;
		}
		RedisProperties redisProperties = bindResult.get();
		String redisConfigKey = redisProperties.getRedisConfigKey();
		if (StringUtils.isBlank(redisConfigKey)) {
			log.debug("redisConfigKey is not config, ignore AliYun Redis auto configuration");
			return;
		}

		String zkServer = redisProperties.getRedisZookeeperServer();
		if (StringUtils.isBlank(zkServer)) {
			if (DeployEnvEnum.isPre()) {
				zkServer = ZK_SERVER_PRE;
			} else if (DeployEnvEnum.isPrd()) {
				zkServer = ZK_SERVER_PRD;
			} else {
				zkServer = ZK_SERVER_TEST;
			}
		}
		RedisInfo redisInfo;
		try {
			redisInfo = getRedisInfo(zkServer, redisConfigKey);
		} catch (Exception ex) {
			throw new RuntimeException("get redis config info from zookeeper failed. zkServer: " + zkServer
					+ ", redisConfigKey: " + redisConfigKey, ex);
		}

		JSONObject map = new JSONObject();
		map.put("spring.redis.host", redisInfo.getHost());
		map.put("spring.redis.port", redisInfo.getPort());
		map.put("spring.redis.database", redisInfo.getDbNo());
		map.put("spring.redis.timeout", "1s");
		log.info("AliYun Redis config info: " + map.toJSONString() + ", zkServer: " + zkServer + ", redisConfigKey: "
				+ redisConfigKey);
		map.put("spring.redis.password", redisInfo.getPassword());

		MutablePropertySources propertySources = environment.getPropertySources();
		addOrReplace(propertySources, map);
	}

	private RedisInfo getRedisInfo(String server, String redisConfigKey) throws Exception {
		ZooKeeper zooKeeper = new ZooKeeper(server, 5000, (event) -> log.info(event.toString()));
		String data = new String(zooKeeper.getData(redisConfigKey, false, null), CacheConstants.DEFAULT_CHARSET);
		RedisInfo redisInfo = JSON.parseObject(data, RedisInfo.class);
		zooKeeper.close();
		Objects.requireNonNull(redisInfo, "无效的 redisConfigKey, 请检查是否配置错误");
		return redisInfo;
	}

	private void addOrReplace(MutablePropertySources propertySources, Map<String, Object> map) {
		MapPropertySource target = null;
		PropertySource<?> bootstrapProperties = propertySources.get(BOOTSTRAP_PROPERTY_SOURCE_NAME);
		if (bootstrapProperties instanceof CompositePropertySource) {
			// 使用了 spring-cloud-context/spring-cloud-starter-config 才会有
			CompositePropertySource composite = (CompositePropertySource) bootstrapProperties;
			for (PropertySource<?> propertySource : composite.getPropertySources()) {
				if ("configService".equals(propertySource.getName())
						&& propertySource instanceof CompositePropertySource) {
					// {@link org.springframework.cloud.config.client.ConfigServicePropertySourceLocator#locate}
					CompositePropertySource configService = (CompositePropertySource) propertySource;
					target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
					configService.addPropertySource(target);
					return;
				}
			}
		}

		PropertySource<?> source = propertySources.get(PROPERTY_SOURCE_NAME);
		if (source instanceof MapPropertySource) {
			target = (MapPropertySource) source;
			for (Entry<String, Object> entry : map.entrySet()) {
				String key = entry.getKey();
				if (!target.containsProperty(key)) {
					target.getSource().put(key, entry.getValue());
				}
			}
		}
		if (target == null) {
			target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
		}
		if (!propertySources.contains(PROPERTY_SOURCE_NAME)) {
			propertySources.addLast(target);
		}
	}

}
